/*
 * Copyright (c) 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may obtain
 * a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

/**
 * @fileoverview Logic for the Yes/No/Maybe app.
 *
 * @author Tim Blasi (Google)
 */

var last_render = null;
var last_fullrender = null;
var completed_change = new Array;
/**
 * Shared state of the app.
 * @type {Object.<!string, !string>}
 * @private
 */
var state_ = null;

/**
 * Describes the shared state of the object.
 * @type {Object.<!string, Object.<!string, *>>}
 * @private
 */
var metadata_ = null;

/**
 * A list of the participants.
 * @type {Array.<gapi.hangout.Participant>}
 * @private
 */
var participants_ = null;


/**
 * Executes the provided function after a minor delay.
 * @param {function()} func The function to execute.
 */
function defer(func) {
    window.setTimeout(func, 10);
}

/**
 * Creates a key for use in the shared state.
 * @param {!string} id The user's temporary id.
 * @param {!string} key The property to create a key for.
 * @return {!string} A new key for use in the shared state.
 */
function makeUserKey(id, key) {
    return id + ':' + key;
}

/**
 * Makes an RPC call to store the given value(s) in the shared state.
 * @param {!(string|Object.<!string, !string>)} keyOrState Either an object
 *     denoting the desired key value pair(s), or a single string key.
 * @param {!string=} opt_value If keyOrState is a string, the associated value.
 */
var saveValue = null;

/**
 * Makes an RPC call to remove the given value(s) from the shared state.
 * @param {!(string|Array.<!string>)} keyOrListToRemove A single key
 *     or an array of strings to remove from the shared state.
 */
var removeValue = null;

/**
 * Makes an RPC call to add and/or remove the given value(s) from the shared
 * state.
 * @param {?(string|Object.<!string, !string>)} addState  Either an object
 *     denoting the desired key value pair(s), or a single string key.
 * @param {?(string|Object.<!string, !string>)=} opt_removeState A list of keys
 *     to remove from the shared state.
 */
var submitDelta = null;

(function () {
    /**
     * Packages the parameters into a delta object for use with submitDelta.
     * @param {!(string|Object.<!string, !string>)}  Either an object denoting
     *     the desired key value pair(s), or a single string key.
     * @param {!string=} opt_value If keyOrState is a string, the associated
     *     string value.
     */
    var prepareForSave = function (keyOrState, opt_value) {
        var state = null;
        if (typeof keyOrState === 'string') {
            state = {};
            state[keyOrState] = opt_value;
        } else if (typeof keyOrState === 'object' && null !== keyOrState) {
            // Ensure that no prototype-level properties are hitching a ride.
            state = {};
            for (var key in keyOrState) {
                if (keyOrState.hasOwnProperty(key)) {
                    state[key] = keyOrState[key];
                }
            }
        } else {
            throw 'Unexpected argument.';
        }
        return state;
    };

    /**
     * Packages one or more keys to remove for use with submitDelta.
     * @param {!(string|Array.<!string>)} keyOrListToRemove A single key
     *     or an array of strings to remove from the shared state.
     * @return {!Array.<!string>} A list of keys to remove from the shared state.
     */
    var prepareForRemove = function (keyOrListToRemove) {
        var delta = null;
        if (typeof keyOrListToRemove === 'string') {
            delta = [keyOrListToRemove];
        } else if (typeof keyOrListToRemove.length === 'number' &&
            keyOrListToRemove.propertyIsEnumerable('length')) {
            // Discard non-string elements.
            for (var i = 0, iLen = keyOrListToRemove.length; i < iLen; ++i) {
                if (typeof keyOrListToRemove[i] === 'string') {
                    delta.push(keyOrListToRemove[i]);
                }
            }
        } else {
            throw 'Unexpected argument.';
        }
        return delta;
    };

    /**
     * Makes an RPC call to add and/or remove the given value(s) from the shared
     * state.
     * @param {?(string|Object.<!string, !string>)} addState  Either an object
     *     denoting the desired key value pair(s), or a single string key.
     * @param {?(string|Object.<!string, !string>)=} opt_removeState A list of
     *     keys to remove from the shared state.
     */
    var submitDeltaInternal = function (addState, opt_removeState) {
        gapi.hangout.data.submitDelta(addState, opt_removeState);
    };

    saveValue = function (keyOrState, opt_value) {
        var delta = prepareForSave(keyOrState, opt_value);
        if (delta) {
            submitDeltaInternal(delta);
        }
    };

    removeValue = function (keyOrListToRemove) {
        var delta = prepareForRemove(keyOrListToRemove);
        if (delta) {
            submitDeltaInternal({}, delta);
        }
    };

    submitDelta = function (addState, opt_removeState) {
        if ((typeof addState !== 'object' && typeof addState !== 'undefined') ||
            (typeof opt_removeState !== 'object' &&
                typeof opt_removeState !== 'undefined')) {
            throw 'Unexpected value for submitDelta';
        }
        var toAdd = addState ? prepareForSave(addState) : {};
        var toRemove = opt_removeState ? prepareForRemove(opt_removeState) :
            undefined;
        submitDeltaInternal(toAdd, toRemove);
    };
})();


/**
 * Gets the value of opt_stateKey in the shared state, or the entire state
 * object if opt_stateKey is null or not supplied.
 * @param {?string=} opt_stateKey The key to get from the state object.
 * @return {(string|Object.<string,string>)} A state value or the state object.
 */
function getState(opt_stateKey) {
    return (typeof opt_stateKey === 'string') ? state_[opt_stateKey] : state_;
}

/**
 * Gets the value of opt_metadataKey in the shared state, or the entire
 * metadata object if opt_metadataKey is null or not supplied.
 * @param {?string=} opt_metadataKey The key to get from the metadata object.
 * @return {(Object.<string,*>|Object<string,Object.<string,*>>)} A metadata
 *     value or the metadata object.
 */
function getMetadata(opt_metadataKey) {
    return (typeof opt_metadataKey === 'string') ? metadata_[opt_metadataKey] :
        metadata_;
}

/**
 * @return {string} The user's ephemeral id.
 */
function getUserHangoutId() {
    return gapi.hangout.getParticipantId();
}


/**
 * Syncs local copies of shared state with those on the server and renders the
 *     app to reflect the changes.
 * @param {!Object.<!string, !string>} state The shared state.
 * @param {!Object.<!string, Object.<!string, *>>} metadata Data describing the
 *     shared state.
 */
function updateLocalDataState(state, metadata) {
    state_ = state;
    metadata_ = metadata;
    render();
}

/**
 * Syncs local copy of the participants list with that on the server and renders
 *     the app to reflect the changes.
 * @param {!Array.<gapi.hangout.Participant>} participants The new list of
 *     participants.
 */
function updateLocalParticipantsData(participants) {
    participants_ = participants;
    render();
}


(function () {
    if (gapi && gapi.hangout) {

        var initHangout = function (apiInitEvent) {
            if (apiInitEvent.isApiReady) {
                prepareAppDOM();

                gapi.hangout.data.onStateChanged.add(function (stateChangeEvent) {
                    updateLocalDataState(stateChangeEvent.state,
                        stateChangeEvent.metadata);
                });
                gapi.hangout.onParticipantsChanged.add(function (partChangeEvent) {
                    updateLocalParticipantsData(partChangeEvent.participants);
                });

                if (!state_) {
                    var state = gapi.hangout.data.getState();
                    var metadata = gapi.hangout.data.getStateMetadata();
                    if (state && metadata) {
                        updateLocalDataState(state, metadata);
                    }
                }
                if (!participants_) {
                    var initParticipants = gapi.hangout.getParticipants();
                    if (initParticipants) {
                        updateLocalParticipantsData(initParticipants);
                    }
                }

                gapi.hangout.onApiReady.remove(initHangout);
            }
        };

        gapi.hangout.onApiReady.add(initHangout);

    }
})();